#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define dbg printf
#define message printf

enum {
	E_CMDSYNTAX = 256,
	E_NOTHING,
	E_DBSYNTAX,
};

struct blob {
	char *buf;
	int size;
};

struct kv_pair {
	struct blob *key;
	struct blob *value;
};

struct kv_list {
	struct kv_pair *p;
	struct kv_list *next;
};

void help(void)
{
	printf ("Usage:\n");
	printf ("streamrv add TAG ARGS\n");
	printf ("streamrv edit TAG ARGS\n");
}

struct blob *make_blob (char *data, int count)
{
	char *p;
	struct blob *b;
	p = malloc (count+1);
	if (!p)
		return NULL;
	b = malloc (sizeof (struct blob));
	if (!b) {
		free (p);
		return NULL;
	}
	memcpy (p, data, count);
	p [count] = '\0';
	b->buf = p;
	b->size = count;
	return b;
}

int blob_cmp (struct blob *b1, struct blob *b2)
{
	int f;
	if (b1->size==0 && b2->size==0)
		return 0;
	if (b1->size==0) return 1;
	if (b2->size==0) return -1;
	f = memcmp (b1->buf, b2->buf, (b1->size < b2->size) ? b1->size : b2->size);
	if (f) return f;
	if (b1->size == b2->size) return 0;
	return (b1->size < b2->size) ? 1 : -1;
}

void free_blob (struct blob *b)
{
	if (b->buf) free (b->buf);
	memset (b, 0x00, sizeof (*b));
	free (b);
}

struct kv_pair *make_kv_pair (char *key, int keylen, char *val, int vallen)
{
	struct blob *a, *b;
	struct kv_pair *p;
	a = make_blob (key, keylen);
	if (!a)
		return NULL;
	b = make_blob (val, vallen);
	if (!b) {
		free_blob (a);
		return NULL;
	}
	p = malloc (sizeof (struct kv_pair));
	if (!p) {
		free_blob (a);
		free_blob (b);
		return NULL;
	}
	p->key = a;
	p->value = b;
	return p;
}

int parse_kv_pair (char *str, struct kv_pair **res)
{
	int i;
	for (i=0; str [i] != '\0' && str [i] != '='; i++);
	if (i && str [i] == '=') {
		struct kv_pair *p = make_kv_pair(str, i, str+i+1, strlen(str)-i-1);
		if (!p) return ENOMEM;
		*res = p;
		return 0;
	}
	return E_CMDSYNTAX;
		
}

int kv_add_after (struct kv_list *last_elem, struct kv_pair *elem)
{
	struct kv_list *newcell = malloc (sizeof (struct kv_list));
	if (!newcell)
		return ENOMEM;
	newcell->p = elem;
	newcell->next = last_elem->next /* NULL if really last */;
	last_elem->next = newcell;
	return 0;
}

int kv_list_append (struct kv_list *list, struct kv_pair *elem)
{
	struct kv_list *p = list;
	while (p->next)
		p = p->next;
	return kv_add_after (p, elem);
}

int kv_add_if_absent (struct kv_list *list, struct kv_pair *elem)
{
	struct kv_list *p = list;
	while (p->next) {
		if (blob_cmp (list->p->key, elem->key) == 0)
			return;
		p = p->next;
	}
	return kv_add_after (p, elem);
}

void free_kv_pair (struct kv_pair *p)
{
	if (p->key) free_blob (p->key);
	if (p->value) free_blob (p->value);
	memset (p, 0x0, sizeof (*p));
	free (p);
}

struct kv_list *make_kv_list(struct kv_pair *elem)
{
	struct kv_list  *node;
	node = malloc (sizeof(struct kv_list));
	if (!node)
		return NULL;
	node->p = elem;
	node->next = NULL;
	return node;
}

void free_kv_list(struct kv_list *list)
{
	struct kv_list *p, *next = NULL;
	for (p=list; p;) {
		struct kv_list *cur = p;
		 p = p->next;
		 free_kv_pair (cur->p);
		memset (cur, 0x0, sizeof (*cur));
	}
}

const char *dbfn = "test.db";

struct filehooks {
	FILE *fh;
	void *data;
	int (*start_hook) (struct filehooks *data);
	int (*next_hook) (char *buf, size_t size, struct filehooks *data);
	int (*end_hook) (struct filehooks *data);
};

void fh_set_file (struct filehooks *f, FILE *fh)
{
	f->fh = fh;
}

int db_preparse_chunk (int *id, char **ptr, int *size, char *buf, size_t len)
{
	long idlocal, sizelocal;
	char *buflocal = buf;
	int ch;
	errno = 0;
	/* FIXME. this implies that buf [len] is 0 */
	idlocal = strtol(buf, &buflocal, 10);
	if (!idlocal || errno || *buflocal !=',')
		return E_DBSYNTAX;
	buflocal++;
	sizelocal = 0;
	while (sizelocal<len && buflocal [sizelocal]!=',')
		sizelocal++;
	if (sizelocal==len)
		return E_DBSYNTAX;
	*id = idlocal;
	*ptr = buflocal;
	*size = sizelocal;
	return 0;
}

int db_write_record (int id, struct kv_list *rec, FILE *fh)
{
	struct kv_list *p;
	int ret;
	for (p=rec; p!=NULL; p=p->next) {
		ret = fprintf(fh, "%d,%s,%s\n", id, p->p->key->buf, p->p->value->buf);
		if (ret < 0) return errno;
	}
	ret = fprintf (fh, "%d,%s,\n", id, "endrec");
	if (ret < 0) return errno;
	return 0;
}

struct add_ctx {
	id_t last_id;
	int exists;
	struct kv_list *fields;
};

struct del_ctx {
	id_t deleted_id;
	struct kv_list *fields;
};

struct edit_ctx {
	id_t edited_id;
	struct kv_list *fields;
};

struct list_ctx {
	struct kv_list *fields;
};

int db_parse_chunk_tail (char *buf, struct kv_pair **res)
{
	int i,j;
	struct kv_pair *p;
	i=0;
	while (buf [i]!=',' && buf [i]!='\0') i++;
	if (buf [i]=='\0') return E_DBSYNTAX;
	j=i+1;
	while (buf [j] != '\n' && buf [j] != '\0') j++;
	if (buf [j]=='\0') return E_DBSYNTAX;
	p = make_kv_pair (buf, i, buf+i+1, j-i-1);
	if (!p) return ENOMEM;
	*res = p;
	return 0;
}

int hook_next_add (char *buf, size_t len, struct filehooks *hooks)
{
	id_t id;
	char *ptr;
	int size;
	struct add_ctx *ctx = (struct add_ctx *)hooks->data;
	fprintf (hooks->fh, "%s", buf);
	db_preparse_chunk(&id, &ptr, &size, buf, len);
	if (ctx->last_id != id) {
		struct kv_pair *p = NULL;
		int ret;
		ret = db_parse_chunk_tail(ptr, &p);
		if (ret)
			return ret;
		if (blob_cmp (p->value, ctx->fields->p->value) == 0) {
			ctx->exists = 1;
		}
		free_kv_pair(p);
	}
	ctx->last_id = id;
	return 0;
}

int hook_end_add (struct filehooks *hooks)
{
	struct add_ctx *ctx = (struct add_ctx *)hooks->data;
	if (!ctx->exists) {
		int id = ctx->last_id+1;
		db_write_record (id, ctx->fields, hooks->fh);
	}
}

int hook_new_add (struct filehooks *hooks, struct kv_list * args)
{
	struct add_ctx *p;
	p = malloc (sizeof(struct add_ctx));
	if (!p) return ENOMEM;
	p->last_id = 0;
	p->exists = 0;
	p->fields = args;
	hooks->data = p;
	return 0;
}

int hook_next_del (char* buf, size_t len, struct filehooks *hooks)
{
	int id;
	char *ptr;
	int size;
	struct del_ctx *ctx = (struct del_ctx *)hooks->data;
	db_preparse_chunk (&id, &ptr, &size, buf, len);
	if (id == ctx->deleted_id)
		return 0;
	if (len==3 && memcpy(ptr,"tag",3)==0) {
		struct kv_pair *p = NULL;
		int ret;
		ret = db_parse_chunk_tail(ptr, &p);
		if (ret) return ret;
		if (blob_cmp (p->value, ctx->fields->p->value) == 0) {
			ctx->deleted_id = id;
		}
		free_kv_pair (p);
	}
	if (id == ctx->deleted_id)
		return 0;
	fprintf (hooks->fh, "%s", buf);
	return 0;
}

int hook_next_edit (char *buf, size_t len, struct filehooks *hooks)
{
	int id;
	char *ptr;
	int size;
	struct edit_ctx *ctx = (struct edit_ctx *)hooks->data;
	db_preparse_chunk (&id, &ptr, &size, buf, len);
	if (id == ctx->edited_id) {
		struct kv_pair *p = NULL;
		int ret;
		if (size==6 && memcpy(ptr, "endrec", 6) == 0) {
			db_write_record (id, ctx->fields, hooks->fh);
			free_kv_list (ctx->fields);
			ctx->fields = NULL;
			return 0;
		}
		ret = db_parse_chunk_tail(ptr, &p);
		if (ret) return ret;
		/* FIXME. leaks */
		kv_add_if_absent (ctx->fields, p);
		return 0;
	}
	if (len==3 && memcpy(ptr,"tag",3)==0) {
		struct kv_pair *p = NULL;
		int ret;
		ret = db_parse_chunk_tail(ptr, &p);
		if (ret) return ret;
		if (blob_cmp (p->value, ctx->fields->p->value) == 0) {
			ctx->edited_id = id;
		}
		free_kv_pair (p);
		return 0;
	}
	fprintf (hooks->fh, "%s", buf);
	return 0;
}

struct kv_list *kv_find_key(struct kv_list *list, const char *key);

#define field_value(h,x,p) ((p = kv_find_key(h,x))?p->p->value:NULL)

int fmt_print_ussr1 (struct kv_list *fields)
{
	struct blob *author = NULL, *title = NULL;
	struct blob *pubcity = NULL, *pub = NULL, *pubyear = NULL;
	struct kv_list *p;

	author  = field_value(fields, "author", p);
	title   = field_value(fields, "title", p);
	pubcity = field_value(fields, "pubcity", p);
	pub     = field_value(fields, "pub", p);
	pubyear = field_value(fields, "pubyear", p);

	if (!title) return 1;

	printf ("<p>%s. <i>%s</i>%s%s%s%s%s\n",
		author ? author->buf: "",
		title ? title->buf: "",
		pubcity ? pubcity->buf : "",
		pubcity ? ":" : "",
		pub ? pub->buf : "",
		pubyear ? ", " : "",
		pubyear ? pubyear->buf : "");

	return 0;
}

int fmt_print (int fmt, struct kv_list *fields)
{
	struct kv_list *p;
	p = kv_find_key(p, "format");
	if (!p || !strcmp (p->p->value->buf, "ussr1")) {
		return fmt_print_ussr1 (fields);
	}
	return 1;
}

int hook_start_list (struct filehooks *hooks)
{
	printf ("<html><body>\n");
}

int hook_next_list (char *buf, size_t len, struct filehooks *hooks)
{
	int id;
	char *ptr;
	int size;
	int ret;
	struct kv_pair *p = NULL;
	struct list_ctx *ctx = (struct list_ctx *)hooks->data;
	db_preparse_chunk (&id, &ptr, &size, buf, len);
	if (size==6 && memcpy(ptr,"endrec",6)==0) {
		fmt_print(0, ctx->fields);
		free_kv_list (ctx->fields);
		ctx->fields = NULL;
		return 0;
	}
	ret = db_parse_chunk_tail(ptr, &p);
	if (ret) return ret;
	if (ctx->fields)
		kv_list_append (ctx->fields, p);
	else
		ctx->fields = make_kv_list (p);
	return 0;
}

int hook_end_list (struct filehooks *hooks)
{
	printf ("</body></html>\n");
}

enum {
	CMD_HELP,
	CMD_ADD,
	CMD_EDIT,
	CMD_DELETE,
	CMD_LIST,
};

struct commands {
	char *cmd;
} commands [] = {
	/* CMD_HELP */
	{ "help" },
	/* CMD_ADD */
	{ "add" },
	/* CMD_EDIT */
	{ "edit" },
	/* CMD_DELETE */
	{ "delete" },
	/* CMD_LIST */
	{ "list"},
};

int find_cmd (const char *str)
{
	int i;
	for (i=0; i<sizeof(commands)/sizeof(struct commands); i++)
		if (strcmp (commands[i].cmd, str) == 0)
			return i;
	return -1;
}

int check_tag_syntax(const char *tag)
{
	const char *p;
	if (!isalpha(*tag) || !isupper(*tag))
		return -1;
	for (p=tag+1; *p; p++)
		if (!isdigit(*p))
			return -1;
	return 0;
}

void set_hook_data (struct filehooks *hooks, int cmd, struct kv_list *args)
{
	switch (cmd) {
	case CMD_ADD:
		hook_new_add (hooks, args);
		hooks->start_hook = NULL;
		hooks->next_hook = hook_next_add;
		hooks->end_hook = hook_end_add;
		break;
	case CMD_EDIT:
		hook_new_edit (hooks, args);
		hooks->start_hook = NULL;
		hooks->next_hook = hook_next_edit;
		hooks->end_hook = hook_end_add;
		break;
	case CMD_DELETE:
		hooks_new_delete (hooks, args);
		hooks->start_hook = NULL;
		hooks->next_hook = hook_next_del;
		hooks->end_hook = NULL;
		break;
	case CMD_LIST:
		hooks_new_list (hooks, args);
		hooks->start_hook = hook_start_list;
		hooks->next_hook = hook_next_list;
		hooks->end_hook = hook_end_list;
		break;
	}
}

char *aux_file_name (const char *fn)
{
	size_t len;
	char *auxfn;
	if (!fn) return NULL;
	len = strlen (fn)+3;
	auxfn = malloc (len);
	if (!auxfn) return NULL;
	sprintf (auxfn, "%s.1", fn);
	return auxfn;
}

#define BUFSIZE 4096

int cycle (const char *fn, struct filehooks *hooks)
{
	FILE *F, *G;
	char *fn1;
	char buf [BUFSIZE];
	fn1 = aux_file_name (fn);
	int ret;
	if (!fn1) {
		return EIO;
	}
	if ((G = fopen (fn1, "w")) == NULL) {
		message ("cannot open new db file \"%s\"\n", fn1);
		return EIO;
	}
	if ((F = fopen (fn, "r")) == NULL) {
		message ("cannot open db file \"%s\".\n", dbfn);
		fclose (G);
		return EIO;
	}
	fh_set_file (hooks, G);
	ret = hooks->start_hook ? hooks->start_hook (hooks) : 0;
	if (ret) {
		fclose (F);
		fclose (G);
		return ret;
	}
	while (fgets (buf, BUFSIZE, F)) {
		ret = hooks->next_hook? hooks->next_hook (buf, strlen (buf), hooks) : 0;
		if (ret) {
			fclose (F);
			fclose (G);
			return ret;
		}
#if 0		
		if (fputs (buf, G) == EOF) {
			message ("rewriting IO error.\n");
			fclose (G);
			fclose (F);
			return EIO;
		}
#endif
	}
	ret = hooks->end_hook ? hooks->end_hook (hooks) : 0;
	if (ret) {
		fclose (F);
		fclose (G);
		return ret;
	}
	fclose (F);
	fclose (G);
	if (unlink (fn) == -1) {
		message ("cannot unlink db file.\n");
		return EIO;
	}
	if (rename (fn1, fn) == -1) {
		message ("cannot update db file.\n");
		return EIO;
	}
	free (fn1);
	return 0;
}

int readonly_cycle(const char *fn, struct filehooks *hooks)
{
	int ret;
	FILE *F;
	char buf [BUFSIZE];
	if ((F = fopen (fn, "r")) == NULL) {
		message ("cannot open db file \"%s\".\n", dbfn);
		return EIO;
	}
	fh_set_file (hooks, NULL);
	ret = hooks->start_hook ? hooks->start_hook (hooks) : 0;
	if (ret) {
		fclose (F);
		return ret;
	}
	while (fgets (buf, BUFSIZE, F)) {
		ret = hooks->next_hook? hooks->next_hook (buf, strlen (buf), hooks) : 0;
		if (ret) {
			fclose (F);
			return ret;
		}
	}
	ret = hooks->end_hook ? hooks->end_hook (hooks) : 0;
	if (ret) {
		fclose (F);
		return ret;
	}
	return 0;
}

int parse_cmd(int argc, char **argv, int *pcmd, struct kv_list **pargs)
{
	int cmd;
	int i,ret;
	struct kv_pair *idp;
	struct kv_list *rec;
	if (argc < 2) {
		help ();
		return E_NOTHING;
	}
	cmd = find_cmd (argv [1]);
	if (cmd == CMD_HELP || cmd==-1) {
		help();
		return E_NOTHING;
	}
	if (argc < 3) {
		return E_CMDSYNTAX;
	}
	ret = check_tag_syntax (argv [2]);
	if (ret)
		return ret;
	idp = make_kv_pair ("tag", 3, argv [2], strlen (argv [2]));
	if (!idp) {
		return ENOMEM;
	}
	rec = make_kv_list (idp);
	if (!rec) {
		free_kv_pair (idp);
		return ENOMEM;
	}
	for (i=3; i<argc; i++) {
		struct kv_pair *elem;
		ret = parse_kv_pair (argv [i], &elem);
		if (ret) {
			free_kv_list (rec);
			/* ENOMEM of invalid format */
			return ret;
		}
		ret = kv_list_append (rec, elem);
		if (ret) {
			free_kv_list (rec);
			free_kv_pair (elem);
			/* ENOMEM */
			return ret;
		}
	}
	*pcmd = cmd;
	*pargs = rec;
	return 0;
}

int main (int argc, char **argv)
{
	int ret;
	int cmd;
	struct kv_list *args = NULL;
	struct filehooks hooks;
	ret = parse_cmd (argc, argv, &cmd, &args);
	if (ret) {
		printf ("error parsing cmd: %d\n", ret);
		exit (1);
	}
	set_hook_data (&hooks, cmd, args);
	if (cmd == CMD_LIST)
		ret = readonly_cycle (dbfn, &hooks);
	else
		ret = cycle (dbfn, &hooks);
	return ret;
}

